package com.example.demo.client;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.example.demo.config.CertConstants;
import com.example.demo.config.Configuration;
import com.example.demo.config.ModuleCode;
import com.example.demo.config.SDKVersion;
import com.example.demo.encrypt.AESUtils;
import com.example.demo.encrypt.Encryptor;
import com.example.demo.util.SerialNoUtil;
import com.example.demo.util.VerifyAndDecryptUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.TreeMap;

/**
 * @author tangjunmao
 * @date 2018/9/27
 */
public class FBankOpenApiClient {
    private static final Logger LOGGER = LoggerFactory.getLogger(FBankOpenApiClient.class);
    /**
     * 服务所使用的加密算法
     */
    private static final String ALGORITHM = "algorithm";
    /**
     * 加密算法服务类型
     **/
    private static final String ALGORITHM_TYPE = "rsaDetached";

    /**
     * 签名数据
     **/
    private static final String SIGN_DATA = "signData";



    static {
        trustAllHttpsCertificates();
    }

    /**
     * 配置
     */
    private Configuration config;

    public FBankOpenApiClient(Configuration config) {
        this.config = config;
    }

    /**
     * 设置http 的通用请求头
     *
     * @param connection HttpURLConnection实例
     * @param context 请求参数上下文
     * @param clientSerialNo 请求流水号
     */
    private static void setHttpHeaders(HttpURLConnection connection, OpenParameters context, String clientSerialNo) {
        connection.setRequestProperty("api_name", context.getApiName());
        String apiCode = context.getApiCode();
        if (null != apiCode) {
            connection.setRequestProperty("api_code", apiCode);
        }
        String moduleCode = context.getModuleCode();
        if (null != moduleCode) {
            connection.setRequestProperty("module_code", moduleCode);
        }
        connection.setRequestProperty("api_version", context.getApiVersion());
        connection.setRequestProperty(ALGORITHM, ALGORITHM_TYPE);
        connection.setRequestProperty("client_serial_no", clientSerialNo);
        connection.setRequestProperty("Content-Type", "application/json");
    }

    /**
     * 解析响应结果，关闭输入输出流
     *
     * @param connection HttpURLConnection实例
     * @param encryptedBody 加密报文
     * @param charset 所使用字符集
     */
    private static String getResponse(HttpURLConnection connection, String encryptedBody, String charset)
            throws IOException {
        try (OutputStream outputStream = connection.getOutputStream()) {
            if (null != encryptedBody) {
                // 发送请求数据
                outputStream.write(encryptedBody.getBytes(charset));
                outputStream.flush();
            }
        }

        try (InputStream in = connection.getInputStream()) {
            // 读取数据到字节数组
            byte[] bytes = read(in);
            return (new String(bytes, charset)).trim();
        }
    }

    /**
     * 读取数据到字节数组
     *
     * @param inputStream 输入流
     */
    private static byte[] read(InputStream inputStream) throws IOException {
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            final int defaultBufferSize = 1024 * 8;
            byte[] buffer = new byte[defaultBufferSize];
            for (int num = inputStream.read(buffer); num != -1; num = inputStream.read(buffer)) {
                baos.write(buffer, 0, num);
            }
            baos.flush();
            return baos.toByteArray();
        }
    }

    /**
     * 将某些特定的module_code转化为真正的请求
     *
     * @param context 请求参数上下文
     */
    private static String pseudoModuleCode(OpenParameters context, String address) {
        if (!address.endsWith("/")) {
            address += "/";
        }
        if (ModuleCode.OPENAPI_FUNCTION.rawValue().equalsIgnoreCase(context.getModuleCode())) {
            address += "?function=" + context.getApiCode();
        }
        return address;
    }

    /**
     * 不校验服务端的证书（需要校验时可做处理）
     */
    private static void trustAllHttpsCertificates() {
        try {
            SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
            sslContext.init(null, new TrustManager[]{new X509TrustManager() {
                @Override
                public void checkClientTrusted(X509Certificate[] chain, String authType) {
                }

                @Override
                public void checkServerTrusted(X509Certificate[] chain, String authType) {
                }

                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }
            }}, new SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
            HostnameVerifier hv = new HostnameVerifier() {
                @Override
                public boolean verify(String hostname, SSLSession session) {
                    return hostname.equalsIgnoreCase(session.getPeerHost());
                }
            };
            HttpsURLConnection.setDefaultHostnameVerifier(hv);
        } catch (NoSuchAlgorithmException | KeyManagementException e) {
            LOGGER.error("Invalid ssl for certs.", e);
        }
    }

    public Configuration getConfig() {
        return config;
    }

    public void setConfig(Configuration config) {
        this.config = config;
    }

    /**
     * 建造者模式发送请求
     * We recommend to use this method.
     *
     * @param context params from OpenContext.builder
     * @return response from server
     * @throws Exception all exceptions
     * @since 2.0
     */
    public String send(OpenParameters context) throws Exception {
        System.out.println(JSON.toJSONString(context));
        return sendBuild(context, context.getUserId(), context.getParam());
    }

    private String sendBuild(OpenParameters context, String userId, String params) throws Exception {
        // 对请求参数进行加密和签名【请求体进行加密】
        String encryptedBody = buildBody(userId, params);
        // 校验请求流水号
        String clientSerialNo = context.getSerialNo();
        if (null == clientSerialNo || clientSerialNo.isEmpty()) {
            clientSerialNo = SerialNoUtil.calculateSerialNo(params);
        }
        String encryptedResult = fetch(context, encryptedBody, clientSerialNo);
        JSONObject jsonObject = JSONObject.parseObject(encryptedResult);
        if (null == jsonObject || !jsonObject.containsKey(SIGN_DATA)) {
            return encryptedResult;
        }
        // 使用双证书版本的公钥进行加密
        String fbankSignKey = config.fbankSignPubKey();
        // 签名验证，失败抛出异常
        JSONObject json = VerifyAndDecryptUtil.verifySignature(encryptedResult, fbankSignKey);
        // 解密返回结果
        return VerifyAndDecryptUtil.decryptResult(json, config.privateKey());
    }

    /**
     * 获取响应结果
     *
     * @param context 请求参数上下文
     * @param encryptedBody 加密数据
     * @param clientSerialNo 请求流水号
     * @return 返回数据
     * @throws IOException http请求exception
     */
    private String fetch(OpenParameters context, String encryptedBody, String clientSerialNo) throws IOException {
        String path = pseudoModuleCode(context, config.remoteAddress());
        URL url = new URL(path);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        // 设置http 的通用请求头【通用请求头的设置】
        setHttpHeaders(connection, context, clientSerialNo);
        if (null != encryptedBody) {
            // 需要输出
            connection.setDoOutput(true);
        }
        // 需要输入
        connection.setDoInput(true);
        // 不允许缓存
        connection.setUseCaches(false);
        connection.setRequestMethod("POST");
        // 设置连接超时时间
        connection.setConnectTimeout(config.connectionTimeout());
        // 设置读取超时时间
        connection.setReadTimeout(config.readTimeout());

        // 读取响应结果，关闭输入输出流
        return getResponse(connection, encryptedBody, config.charset());
    }

    /**
     * 对请求参数进行加密和签名
     *
     * @param userId 用户ID
     * @param json   http request parameters
     * @return 加密和签名后的json字符串
     */
    private String buildBody(String userId, String json) throws Exception {
        // 生成AES 随机密钥
        String randomKey = AESUtils.randomKey();
        Encryptor encryptor = VerifyAndDecryptUtil.getEncryptor(config.encryptType());
        // 对AES密钥加密
        String encryptedRandomKey;
        // 使用双证书版本的公钥进行加密
        encryptedRandomKey = encryptor.encrypt(randomKey, config.fbankEncPubKey());

        if (null == json || json.isEmpty()) {
            // A stupid hack for server mustn't be nil.
            JSONObject jsonObject = new JSONObject();
            json = JSONObject.toJSONString(jsonObject);
        }
        // 用AES密钥对请求参数加密
        String data = "";
         if(CertConstants.ENC_RSAV2.equalsIgnoreCase(config.encryptType())) {
             data =  AESUtils.encryptWithCBC(json, randomKey);
              }else {
             data = AESUtils.encrypt(json, randomKey);
         }
        TreeMap<String, Object> tempMap = new TreeMap<>();
        tempMap.put("data", data);
        tempMap.put("randomKeyEncrypt", encryptedRandomKey);
        tempMap.put("timestamp", System.currentTimeMillis());
        tempMap.put("merchantNo", config.merchantNo());
        tempMap.put("channelNo", config.channelNo());
        tempMap.put("signType", config.signType());
        tempMap.put("encryptType", config.encryptType());
        tempMap.put("sdkVersion", SDKVersion.SDK_VERSION);
        if (null != config.appId() && !config.appId().isEmpty()) {
            tempMap.put("appId", config.appId());
            // if appId, we must also have siteId.
            tempMap.put("siteId", config.siteId());
        }
        if (null != userId && !userId.isEmpty()) {
            tempMap.put("userId", userId);
        }
        JSONObject jsonObject = new JSONObject(tempMap);
        String signStr = jsonObject.toJSONString();
        Encryptor signCryptor = VerifyAndDecryptUtil.getEncryptor(config.signType());
        // 对加密后的输入参数签名
        String signedStr = signCryptor.signature(signStr);
        jsonObject.put(SIGN_DATA, signedStr);
        return jsonObject.toJSONString();
    }
}
